﻿using System;
using System.Collections.Generic;
using System.DirectoryServices.Protocols;
using System.Runtime.InteropServices;
using System.Security.Principal;
using ActiveDirectoryUtilities.CommonLibrary.Unsafe;

namespace ActiveDirectoryUtilities.DirectoryServiceAccess
{
	/// <summary>
	/// The value of the msDS-Behavior-Version attribute on the domain's crossRef object
	/// </summary>
	public enum DomainMode
	{
		Windows_2000,
		Windows_2003_Interim,
		Windows_2003,
		Windows_2008,
		Windows_2008_R2,
		Windows8
	}

	public class Domain : IComparable
	{
		/// <summary>
		/// LDAP search filter to find all the domain crossRef objects
		/// </summary>
		internal const string LDAP_SEARCH_FILTER_DOMAIN = "(&(objectCategory=crossRef)(systemFlags:1.2.840.113556.1.4.804:=1)(systemFlags:1.2.840.113556.1.4.804:=2))";
		/// <summary>
		/// Properties to load from the crossRef object that provide information about the domain
		/// </summary>
		internal static readonly string[] domainObjectPropertiesToLoad = new string[] { "distinguishedName", "dnsRoot", "msDS-Behavior-Version", "nCName", "nETBIOSName", "ntMixedDomain", "trustParent" };

		/// <summary>
		/// Get the current domain of the application domain security context
		/// </summary>
		/// <returns>Domain object of the current security context.</returns>
		static public Domain GetCurrentDomain()
		{
			RootDse root = new RootDse();
			return new Domain(root);
		}

		/// <summary>
		/// Get a domain object for a specific domain.
		/// </summary>
		/// <param name="domainName">DNS FQDN or NetBIOS domain name</param>
		/// <returns>Domain object of the specified domain.</returns>
		static public Domain GetDomain(string domainName)
		{
			RootDse root;
			try
			{
				root = new RootDse(domainName);
			}
			catch (LdapException ex)
			{
				if (ex.ErrorCode == 81)
				{
					throw new DirectoryServiceAccessException("Could not find an Active Directory domain controller for the \"" + domainName + "\" domain.");
				}
				else
				{
					throw ex;
				}
			}
			return new Domain(root);
		}

		#region private fields
		private string _dnsRoot, _crossRefDn, _nCName, _netBiosName, _trustParent;
		private bool _ntMixedDomain = false;
		private DomainMode _msDSBehaviorVersion = (DomainMode)0;	//documentation says if null then assume 0.  Leaving hard coded as number to represent the raw data, should the meanings change.
		private List<DomainController> _domainControllers;
		#endregion

		#region public properties
		/// <summary>
		/// Returns the FQDN domain name of this domain
		/// </summary>
		public string DnsDomainName { get { return _dnsRoot; } }
		/// <summary>
		/// Returns the distinguished name of this domain's crossRef object in the configuration naming context
		/// </summary>
		public string CrossRefDN { get { return _crossRefDn; } }
		/// <summary>
		/// Returns the distinguished name of the domain
		/// </summary>
		public string NcName { get { return _nCName; } }
		/// <summary>
		/// Returns the NetBIOS short name of the domain
		/// </summary>
		public string NetBiosName { get { return _netBiosName; } }
		/// <summary>
		/// Returns true if the domain is in Mixed Mode (can replicate to NT 4.0 DCs)
		/// </summary>
		public bool IsNtMixedDomain { get { return _ntMixedDomain; } }
		/// <summary>
		/// Returns the distinguished name of the parent domain.<para>This is null for the forest root domain.</para>
		/// </summary>
		public string TrustParent { get { return _trustParent; } }
		/// <summary>
		/// Returns the Domain Functional Level of this domain.  
		/// </summary>
		public DomainMode MsDsBehaviorVersion { get { return _msDSBehaviorVersion; } }
		/// <summary>
		/// Returns the DomainController object for the PDC of the domain
		/// </summary>
		public DomainController PdcRoleOwner
		{
			get
			{
				foreach (DomainController dc in this.DomainControllers)
				{
					if (dc.IsPdc)
					{
						return dc;
					}
				}
				return null;
			}
		}
		/// <summary>
		/// Expose the list of domain controllers in the domain
		/// </summary>
		public List<DomainController> DomainControllers
		{
			get
			{
				if (_domainControllers == null)
				{
					return this.FindAllDomainControllers();
				}
				return _domainControllers;
			}
		}
		#endregion

		#region c'tors
		private Domain(RootDse root)
		{
			WLdapSearchRequest domainSearch =
				new WLdapSearchRequest(root.DnsHostName
				, 389
				, "CN=Partitions," + root.ConfigurationNamingContext
				, "(&(" + LDAP_SEARCH_FILTER_DOMAIN + ")(nCName=" + root.DefaultNamingContext + "))"
				, SearchScope.Subtree
				, domainObjectPropertiesToLoad);

			SearchResultEntry sre = domainSearch.FindOne();
			populateDomainInfo(sre);
		}

		internal Domain(SearchResultEntry sre)
		{
			populateDomainInfo(sre);
		}
		#endregion

		#region Public Methods
		/// <summary>
		/// If the replication topology is unstable and/or incomplete this can ensure that all DCs in the domain are found by querying each and every DC for a list of all DCs.
		/// <para>This will refresh the cache every time it is called.</para>
		/// </summary>
		/// <param name="assumeUnstableReplicationTopology">Set to true to query every DC in the domain for a list of DCs to ensure the list is most complete.</param>
		/// <returns>List of DomainController objects</returns>
		public List<DomainController> FindAllDomainControllers(bool assumeUnstableReplicationTopology)
		{
			return getDCsInDomain(null, assumeUnstableReplicationTopology);
		}

		/// <summary>
		/// Gets a list of DCs in the domain from the perspective of the nearest DC.<para>This will refresh the cache every time it is called.</para>
		/// </summary>
		/// <returns>List of DomainController objects</returns>
		public List<DomainController> FindAllDomainControllers()
		{
			return getDCsInDomain(null, false);
		}

		/// <summary>
		/// Get all the trusts for the domain specified.
		/// </summary>
		/// <returns>A list of domain trust objects for the specified domain.</returns>
		public List<DomainTrust> GetAllTrustRelationships()
		{
			return this.GetAllTrustRelationships(NetApi32.DS_DOMAIN.ALL);
		}

		/// <summary>
		/// Get all the trusts with the flags specified for the domain specified.
		/// </summary>
		/// <param name="ds_Domain_Flags">DS_DOMAIN bitmask of flags that trusts may contain</param>
		/// <returns>A list of domain trust objects for the specified domain that match the flags specified.</returns>
		public List<DomainTrust> GetAllTrustRelationships(NetApi32.DS_DOMAIN ds_Domain_Flags)
		{
			string serverName = this.PdcRoleOwner.Name;
			NetApi32.DS_DOMAIN_TRUSTS[] trustlist = null;
			List<DomainTrust> trustsInTheDomain = new List<DomainTrust>();
			NetApi32.DsEnumerateDomainTrusts(serverName, ds_Domain_Flags, out trustlist);
			foreach (NetApi32.DS_DOMAIN_TRUSTS dt in trustlist)
			{
				trustsInTheDomain.Add(new DomainTrust(dt, this));
			}
			trustsInTheDomain.Sort();
			return trustsInTheDomain;
		}

		#region IComparable Members

		public int CompareTo(object obj)
		{
			return this.ToString().CompareTo(obj.ToString());
		}

		public override string ToString()
		{
			return this.DnsDomainName;
		}

		#endregion
		#endregion

		#region private methods
		private void populateDomainInfo(SearchResultEntry sre)
		{
			if (sre.Attributes.Contains("dnsRoot"))
			{
				_dnsRoot = sre.Attributes["dnsRoot"][0].ToString();
			}

			if (sre.Attributes.Contains("distinguishedName"))
			{
				_crossRefDn = sre.Attributes["distinguishedName"][0].ToString();
			}

			if (sre.Attributes.Contains("nCName"))
			{
				_nCName = sre.Attributes["nCName"][0].ToString();
			}

			if (sre.Attributes.Contains("ntMixedDomain"))
			{
				if (int.Parse(sre.Attributes["ntMixedDomain"][0].ToString()) == 1)
				{
					_ntMixedDomain = true;
				}
			}
			else
			// Normally, all info in the NC head is propagated to the crossref. However, if there
			// are still Windows 2000 DC's in the forest, it may legitemately happen that the crossref
			// is not populated. In that case, the ABSENSE of the ntMixedDomain on the crossref means that the
			// domain is still in MIXED mode. 
			{
				_ntMixedDomain = true;
			}

			if (sre.Attributes.Contains("msDS-Behavior-Version"))
			{
				_msDSBehaviorVersion = (DomainMode)int.Parse(sre.Attributes["msDS-Behavior-Version"][0].ToString());

			}

			if (sre.Attributes.Contains("nETBIOSName"))
			{
				_netBiosName = sre.Attributes["nETBIOSName"][0].ToString();
			}

			if (sre.Attributes.Contains("trustParent"))
			{
				_trustParent = sre.Attributes["trustParent"][0].ToString();
			}
		}

		private List<DomainController> getDCsInDomain(string domainController, bool assumeUnstableReplicationTopology)
		{
			_domainControllers = null;
			getDCsInDomain(null, assumeUnstableReplicationTopology, ref _domainControllers);
			return _domainControllers;
		}

		private void getDCsInDomain(string domainController, bool assumeUnstableReplicationTopology, ref List<DomainController> results)
		{
			System.Diagnostics.Debug.WriteLine("Collecting list of DCs from \"" + domainController + "\"");
			if (results == null)
			{
				results = new List<DomainController>();
			}

			DomainController.InfoLevel infoLevel = DomainController.InfoLevel.DS_DOMAIN_CONTROLLER_INFO_2;
			NtDsApi.DsHandle hDS = new NtDsApi.DsHandle();
			try
			{
				int startTicks = Environment.TickCount, ticksWaiting = 0;
				uint dsBindResult = NtDsApi.DsBind(domainController, this.DnsDomainName, out hDS);
				if (dsBindResult == 0)
				{
					uint numDcs;
					IntPtr DCInfoStructs;
					uint dsGetDCsResult = NtDsApi.DsGetDomainControllerInfo(hDS, this.DnsDomainName, (uint)infoLevel, out numDcs, out DCInfoStructs);
					if (dsGetDCsResult == 0)
					{
						try
						{
							if ((DCInfoStructs != IntPtr.Zero) && (numDcs != 0))
							{
								IntPtr ptr = IntPtr.Zero;
								for (int i = 0; i < numDcs; i++)
								{
									DomainController dc = null;
									if (infoLevel == DomainController.InfoLevel.DS_DOMAIN_CONTROLLER_INFO_1)
									{
										ptr = (IntPtr)(((long)DCInfoStructs) + (i * Marshal.SizeOf(typeof(NtDsApi.DS_DOMAIN_CONTROLLER_INFO_1))));
										NtDsApi.DS_DOMAIN_CONTROLLER_INFO_1 structure = new NtDsApi.DS_DOMAIN_CONTROLLER_INFO_1();
										Marshal.PtrToStructure(ptr, structure);
										dc = new DomainController(structure);
									}
									else if (infoLevel == DomainController.InfoLevel.DS_DOMAIN_CONTROLLER_INFO_2)
									{
										ptr = (IntPtr)(((long)DCInfoStructs) + (i * Marshal.SizeOf(typeof(NtDsApi.DS_DOMAIN_CONTROLLER_INFO_2))));
										NtDsApi.DS_DOMAIN_CONTROLLER_INFO_2 structure = new NtDsApi.DS_DOMAIN_CONTROLLER_INFO_2();
										Marshal.PtrToStructure(ptr, structure);
										dc = new DomainController(structure);
									}

									ticksWaiting += Environment.TickCount - startTicks;
									if (!results.Contains(dc))
									{
										results.Add(dc);
										if (assumeUnstableReplicationTopology)
										{
											getDCsInDomain(dc.Name, assumeUnstableReplicationTopology, ref results);
										}
									}
									//if ((assumeUnstableReplicationTopology) && (string.Compare(dc.DnsHostName, domainController, StringComparison.OrdinalIgnoreCase) != 0))
									//{
									//    getDCsInDomain(dc.Name, assumeUnstableReplicationTopology, ref results);
									//}
									startTicks = Environment.TickCount;
								}
							}
						}
						finally
						{
							NtDsApi.DsFreeDomainControllerInfo((uint)infoLevel, numDcs, DCInfoStructs);
							ticksWaiting += Environment.TickCount - startTicks;
							TimeSpan waited = new TimeSpan(0, 0, 0, 0, ticksWaiting);
							System.Diagnostics.Debug.WriteLine("\nTime Waiting on DC List:\t" + waited.TotalSeconds.ToString("N0") + "." + waited.Milliseconds.ToString("N0"));
						}
					}
					else
					{
						throw new DirectoryServiceAccessException("Error returned querying DC list:  " + dsBindResult);
					}

					if (dsBindResult == 0)
					{
						results.Sort();
					}
				}
				else if (!assumeUnstableReplicationTopology)
				{
					throw new DirectoryServiceAccessException("Error " + dsBindResult + " getting Domain Controller list from " + domainController + ".");
				}
				//else assume that since the box is down we'll return what we have and that will have to do.
			}
			finally
			{
				hDS.Close();
			}
		}
		#endregion
	}


}
